#pragma once
#include <tiny_gltf.h>
#include "core/cbase.h"
#include "core/CElapsedTimer.h"
#include "PbrMaterial.h"
#include "Enum.h"

struct GltfInfo {
	float width = 0;
	float length;
	float height;
	float maxX;
	float maxY;
	float maxZ;

	bool isValid() const { return width > 0; }
};

class GLTFParser {
public:
	#define INIT init(type);

	static int meshNum(ModelType type) { INIT return s_meshNumMap[type]; }
	static const CString& meshName(ModelType type, int meshIndex = 0) { INIT return s_meshNames[type][meshIndex]; }

	static const CVector<glm::vec3>& vertices(ModelType type, int meshIndex = 0) { INIT return s_vertices[type][meshIndex]; }
	static const CVector<UInt>& indices(ModelType type, int meshIndex = 0) { INIT return s_indices[type][meshIndex]; }
	static const CVector<glm::vec3>& normals(ModelType type, int meshIndex = 0) { INIT return s_normals[type][meshIndex]; }
	static const CVector<glm::vec2>& texcoords(ModelType type, int meshIndex = 0) { INIT return s_texcoords[type][meshIndex]; }
	static const glm::mat4& transform(ModelType type, int meshIndex = 0) { INIT return s_transforms[type][meshIndex]; }
	static const PbrMaterial& material(ModelType type, int meshIndex = 0) { INIT return s_materials[type][meshIndex]; }

	static const GltfInfo& info(ModelType type) { INIT return s_gltfInfoMap[type]; }

	static bool isTrafficBarrier(ModelType type) { return ModelType::Cone == type || ModelType::Barrier == type; }

	static void startAsyncParsingZhiche(const std::function<void()>& completedCallback);

	static const GltfInfo& zhicheInfo();

	static bool isZhicheInfoAvailable();

private:
	GLTFParser() = default;

	/** 
	 * @brief node :: mesh :: vertices/indices/texcoords/normals/material
	 * @note Assume each node with one single mesh(mesh id : int)
	 */
	static CMap<ModelType, int> s_meshNumMap;
	static CMap<ModelType, CMap<int, CString>> s_meshNames;
	static CMap<ModelType, CMap<int, CVector<glm::vec3>>> s_vertices;
	static CMap<ModelType, CMap<int, CVector<GLuint>>> s_indices;
	static CMap<ModelType, CMap<int, CVector<glm::vec3>>> s_normals;
	static CMap<ModelType, CMap<int, CVector<glm::vec2>>> s_texcoords;
	static CMap<ModelType, CMap<int, glm::mat4>> s_transforms;
	static CMap<ModelType, CMap<int, PbrMaterial>> s_materials;
	static CMap<ModelType, GltfInfo> s_gltfInfoMap;

	static GltfInfo s_zhicheInfo;// def invalid
	static bool s_isZhicheParsedDone;

	static void init(ModelType type) {
		if (s_gltfInfoMap.contains(type))
			return;
		parseAndTiming(type);
	}

	static void parseAndTiming(ModelType type, const std::function<void()>& callback = []{}) {
		static CElapsedTimer timer;
		timer.reset();

		parseObj(type);

		CString typeName = "";
		switch (type) {
			case ModelType::Zhiche: typeName = "Zhiche"; break;
			case ModelType::Car: typeName = "Car"; break;
			case ModelType::Bus: typeName = "Bus"; break;
			case ModelType::Truck: typeName = "Truck"; break;
			case ModelType::Moto: typeName = "Moto"; break;
			case ModelType::Bike: typeName = "Bike"; break;
			case ModelType::Cone: typeName = "Cone"; break;
			case ModelType::Person: typeName = "Person"; break;
			case ModelType::Undefined: typeName = "Undefined"; break;
			default: typeName = "Invalid ModelType"; break;
		}
		LogI << "GLTFParser::" << typeName << " model(*.glb) has cached, " << " [took " << timer.elapsed() << "ms]";
		callback();
	}

	/** @warning for CMap map; if using subscripts and no value, a default key-value will be inserted into the map.*/
	static void parseObj(ModelType type);

	/**
	 *@brief get data from Accessor
	 */
	template <typename T>
	static void getAccessorData(CVector<T>& result, const tinygltf::Model& model, int accessorIndex) {
		const tinygltf::Accessor& accessor = model.accessors[accessorIndex];
		const tinygltf::BufferView& bufferView = model.bufferViews[accessor.bufferView];
		const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer];

		const unsigned char* data = buffer.data.data() + bufferView.byteOffset + accessor.byteOffset;
		size_t stride = accessor.ByteStride(bufferView);
		size_t count = accessor.count;

		if constexpr (std::is_same_v<T, UInt>) {// for 'Index'
			if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) {
				for (size_t i = 0; i < count; ++i)
					result << *reinterpret_cast<const unsigned short*>(data + i * stride);
			}
			return;
		}

		for (size_t i = 0; i < count; ++i)
			result << *reinterpret_cast<const T*>(data + i * stride);
	}

	static void parseDataFromNode(const tinygltf::Model& model, const tinygltf::Node& node, const glm::dmat4& parentTransform = glm::dmat4(1.0), const std::string& parentPath = "");

	static glm::mat4 parseNodeTransform(const tinygltf::Node& node, glm::vec3& outTranslation, glm::vec3& outRotation, glm::vec3& outScale) {
		if (!node.matrix.empty()) {// if the node has field: "matrix", just use it directly, so that you don't need to calculate TRS matrix yourself
			auto mat = glm::make_mat4(node.matrix.data());
			decomposeMatrixToTRS(mat, &outTranslation, &outRotation, &outScale);
			return mat;
		} else {
			glm::dmat4 TRSMat(1.0);
			if (!node.translation.empty()) {
				outTranslation = glm::vec3(node.translation[0], node.translation[1], node.translation[2]);
				TRSMat = glm::make_mat4(node.translation.data());
			}
			if (!node.rotation.empty()) {
				const auto& quaternion = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]);
				outRotation = glm::eulerAngles(quaternion);
				TRSMat *= glm::make_mat4(node.rotation.data());
			}
			if (!node.scale.empty()) {
				outScale = glm::vec3(node.scale[0], node.scale[1], node.scale[2]);
				TRSMat *= glm::make_mat4(node.scale.data());
			}
			return glm::mat4(TRSMat);
		}
	}

	static void decomposeMatrixToTRS(const glm::dmat4& transform, glm::vec3* outTranslation = nullptr, glm::vec3* outRotation = nullptr, glm::vec3* outScale = nullptr) {
		// Step 1: Extract the translation part
		if (outTranslation)
			*outTranslation = glm::vec3(transform[3].x, transform[3].y, transform[3].z);
		// Step 2: Calculate scaling section
		if (outScale) {
			*outScale = glm::vec3(
				glm::length(glm::vec3(transform[0])),
				glm::length(glm::vec3(transform[1])),
				glm::length(glm::vec3(transform[2]))
			);
		}
		// Step 3: Remove scaling to obtain rotation matrix
		if (outRotation) {
			glm::vec3 scale;
			if (outScale)
				scale = *outScale;
			else {
				scale = glm::vec3(
					glm::length(glm::vec3(transform[0])),
					glm::length(glm::vec3(transform[1])),
					glm::length(glm::vec3(transform[2]))
				);
			}
			glm::mat3 rotationMat;
			for (int i = 0; i < 3; ++i) {
				glm::vec3 row = glm::vec3(transform[i]);
				row /= scale[i];// Normalize the current row vector
				rotationMat[i] = row;
			}
			// Step 4: Convert the rotation matrix to quaternions
			glm::quat rotationQuat = glm::quat_cast(rotationMat);
			*outRotation = glm::vec3(rotationQuat.x, rotationQuat.y, rotationQuat.z);
		}
	}

	void DecomposeMatrix(const glm::mat4& modelMatrix, glm::vec3& translation, glm::vec3& scale, glm::vec3& rotation) {
		// Extract translation
		translation = glm::vec3(modelMatrix[3]);

		// Extract scale
		scale.x = glm::length(glm::vec3(modelMatrix[0]));
		scale.y = glm::length(glm::vec3(modelMatrix[1]));
		scale.z = glm::length(glm::vec3(modelMatrix[2]));

		// Extract rotation (Euler angle)
		glm::mat3 rotMatrix(
			glm::vec3(modelMatrix[0]) / scale.x,
			glm::vec3(modelMatrix[1]) / scale.y,
			glm::vec3(modelMatrix[2]) / scale.z
		);
		rotation.y = asin(-rotMatrix[2][0]);
		rotation.x = atan2(rotMatrix[2][1], rotMatrix[2][2]);
		rotation.z = atan2(rotMatrix[1][0], rotMatrix[0][0]);
	}

};